﻿using System;
using System.ServiceModel;

namespace LayeredArchitectureSample.Infrastructure.Services
{
    /// <summary>
    /// A helper used to invoke operations on WCF services.
    /// </summary>
    /// <typeparam name="TService">Type that describes the service contract.</typeparam>
    /// <remarks>This must be moved to common, after the restriction of one object operation per delegate is enforced.</remarks>
    public class ServiceOperationInvoker<TService> : IServiceOperationInvoker<TService>
        where TService : class
    {
        private readonly ChannelFactory<TService> _channelFactory;
        private bool _disposed;

        /// <summary>
        /// Constructs a new instance of <see cref="ServiceOperationInvoker{TService}"/>.
        /// </summary>
        /// <exception cref="TimeoutException"></exception>
        public ServiceOperationInvoker(ChannelFactory<TService> channelFactory)
        {
            if (channelFactory == null) throw new ArgumentNullException("channelFactory");

            if (channelFactory.State != CommunicationState.Opened)
            {
                throw new ArgumentException("Expecting an opened ChannelFactory", "channelFactory");
            }

            _channelFactory = channelFactory;
        }

        /// <summary>
        /// Invokes an operation on a remote service.
        /// Note: rethrows all exceptions encountered.
        /// Note: quite limited at the moment, execute no more than one service operation per call.
        /// </summary>
        /// <typeparam name="TResult">Return type of service operation being executed.</typeparam>
        /// <param name="serviceOperationToInvoke">A delegate that describes which service operation must be invoked.</param>
        /// <returns>Value returned as the result of invocatio of service' operation.</returns>
        /// <exception cref="TimeoutException"></exception>
        /// <exception cref="CommunicationException"></exception>
        public TResult Invoke<TResult>(Func<TService, TResult> serviceOperationToInvoke)
        {
            this.MakeSureTheObjectIsNotDisposedOf();

            if (serviceOperationToInvoke == null)
                throw new ArgumentNullException("serviceOperationToInvoke");

            TService serviceProxy = _channelFactory.CreateChannel();

            try
            {
                return serviceOperationToInvoke(serviceProxy);
            }
            finally
            {
                ClientChannel.ClientChannelDispose(serviceProxy);
            }
        }

        /// <summary>
        /// Invokes an operation (that does not have a result) on a remote service.
        /// Note: rethrows all exceptions encountered.
        /// Note: quite limited at the moment, execute no more than one service operation per call.
        /// </summary>
        /// <param name="serviceOperationToInvoke">A delegate that describes which service operation must be invoked.</param>
        /// <exception cref="TimeoutException"></exception>
        /// <exception cref="CommunicationException"></exception>
        public void Invoke(Action<TService> serviceOperationToInvoke)
        {
            this.MakeSureTheObjectIsNotDisposedOf();

            if (serviceOperationToInvoke == null)
                throw new ArgumentNullException("serviceOperationToInvoke");

            TService serviceProxy = _channelFactory.CreateChannel();

            try
            {
                serviceOperationToInvoke(serviceProxy);
            }
            finally
            {
                ClientChannel.ClientChannelDispose(serviceProxy);
            }
        }

        private void MakeSureTheObjectIsNotDisposedOf()
        {
            if (_disposed)
                throw new ObjectDisposedException(this.GetType().FullName);
        }

        /// <summary>
        /// Disposes of the invoker by closing the underlying resources.
        /// </summary>
        public void Dispose()
        {
            if (!_disposed)
            {
                _channelFactory.Close();

                _disposed = true;
            }
        }
    }
}